﻿#region Includes

using System;
using System.IO;
using System.Linq;
using System.Text;
using ArtheaServer.Commands;
using NLog;
using Thought.Net.Telnet;
using ArtheaEngine;
using ArtheaEngine.Model;
using ArtheaServer.Creation;

#endregion

namespace ArtheaServer
{
    public class Connection : IConnection
    {
        private static readonly Logger Log = LogManager.GetCurrentClassLogger();
        private readonly StreamReader _reader;
        private readonly ColorWriter _writer;
        private StringBuilder _pageBuf = new StringBuilder();
        private StateHandler _state;

        public Connection(TelnetClient client, Server server)
        {
            Server = server;
            Client = client;
            client.WillReceived += TelnetWillRecieved;
            client.DoReceived += TelnetDoRecieved;
            client.DontReceived += TelnetDontRecieved;
            client.WontReceived += TelnetWontRecieved;
            client.SubnegotiationReceived += TelnetSubNegotiationRecieved;
            var stream = new TelnetStream(client);
            _writer = new ColorWriter(stream, this) {AutoFlush = true};
            _reader = new StreamReader(stream);
            ScreenHeight = 24;
            ScreenWidth = 80;
            Flags = new Flag();
        }

        public void Close()
        {
            Client.Close();
        }

        public bool HasPage()
        {
            return _pageBuf.Length > 0;
        }

        public World World
        {
            get { return Server.World; }
        }

        public void Run()
        {
            RequestEchoOn();

            WriteLine("Welcome to " + Server.World.Name + "!");

            Write("Account Login: ");
            SetState(GetAccountName);

            while (Server.State != ServerState.Stopped && Client.Parser.Socket.Connected)
            {
                try
                {
                    var line = _reader.ReadLine();
                    if (line == null)
                        break;
                    _state.Invoke(line);
                }
                catch (IOException)
                {
                    break;
                }
            }

            Dispose();
        }

        public void ShowPage(string input)
        {
            if (input.Length > 0)
            {
                _pageBuf.Length = 0;
                return;
            }

            var text = _pageBuf.ToString();

            var length = text.GetLineCountLength(ScreenHeight - 1);

            if (length > 0)
            {
                text = text.Substring(0, length);
                _pageBuf.Remove(0, length);
            }
            else
            {
                _pageBuf.Length = 0;
            }

            Write(text);
        }

        public void Write(string value)
        {
            _writer.Write(value);
        }

        public void WriteLine(string value)
        {
            _writer.WriteLine(value);
        }

        public Account Account { get; set; }

        public TelnetClient Client { get; set; }
        public Flag Flags { get; set; }
        public Server Server { get; set; }
        public Editor Editing { get; set; }

        public string TerminalType { get; private set; }

        #region Telnet Handlers

        private static void TelnetDontRecieved(object o, TelnetOptionEventArgs args)
        {
            //Log.Trace("Recieved Dont " + args.Option);
        }

        private static void TelnetWontRecieved(object o, TelnetOptionEventArgs args)
        {
            //Log.Trace("Recieved Wont " + args.Option);
        }

        private void TelnetDoRecieved(object o, TelnetOptionEventArgs args)
        {
            switch (args.Option)
            {
                case TelnetOption.Echo:
                    if (Flags.And(Connection.SuppressEcho))
                        args.Agreed = true;
                    break;
            }
        }

        private void TelnetSubNegotiationRecieved(object o, TelnetOptionEventArgs args)
        {
            switch (args.Option)
            {
                case TelnetOption.WindowSize:
                    ScreenHeight = BitConverter.ToInt16(args.Data.Reverse().ToArray(), 0);
                    ScreenWidth = BitConverter.ToInt16(args.Data.Reverse().ToArray(), 2);
                    //Log.Trace("Recieved " + args.Option + " " + ScreenWidth + " " + ScreenHeight);
                    break;
                case TelnetOption.TerminalType:
                    TerminalType = Encoding.Default.GetString(args.Data.Skip(1).ToArray());
                    break;
            }
        }

        private void TelnetWillRecieved(object o, TelnetOptionEventArgs args)
        {
            switch (args.Option)
            {
                case TelnetOption.WindowSize:
                    args.Agreed = true;
                    break;
                case TelnetOption.TerminalType:
                    args.Agreed = true;
                    Client.SendSubnegotiation(TelnetOption.TerminalType);
                    break;
                case TelnetOption.SuppressGoAhead:
                    args.Agreed = true;
                    break;
                case TelnetOption.EndOfRecord:
                    args.Agreed = true;
                    break;
                case TelnetOption.Echo:
                    args.Agreed = true;
                    break;
            }
        }

        #endregion

        #region IConnection Members

        public Player Playing { get; set; }
        public short ScreenHeight { get; private set; }

        public short ScreenWidth { get; private set; }

        public void WriteLine(string format, params object[] args)
        {
            _writer.WriteLine(format, args);
        }

        public void Write(string format, params object[] args)
        {
            _writer.Write(format, args);
        }

        public void Write(byte[] value)
        {
            Client.Write(value, 0, value.Length);
        }

        public void Write(object obj)
        {
            _writer.Write(obj);
        }

        public void WriteLine()
        {
            _writer.WriteLine();
        }

        public void WriteLine(object obj)
        {
            _writer.WriteLine(obj);
        }

        public void Page(StringBuilder buf)
        {
            if (HasPage())
            {
                _pageBuf.Append(buf);
            }
            else
            {
                _pageBuf = buf;
                ShowPage(string.Empty);
            }
        }

        public bool HasColor()
        {
            return true;
        }

        #endregion

        #region State Handlers

        private byte _passwordCount;

        private delegate void StateHandler(string argument);

        private void AccountPlayerSelection()
        {
            WriteLine();
            WriteLine("C) Create New Character");
            for (var i = 0; i < Account.Players.Count; i++)
            {
                WriteLine("{0}) {1}", i + 1, Account.Players.ElementAt(i).Name);
            }
            Write("What character would you like to play? ");
        }

        private void ConfirmNewAccount(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                ConfirmNewAccount("Y");
                return;
            }
            switch (char.ToUpper(argument[0]))
            {
                default:
                    Write("Enter a password for " + Account.Login + ": ");
                    SetState(GetNewPassword);
                    RequestEchoOff();
                    break;
                case 'N':
                    Write("Login: ");
                    Account = null;
                    SetState(GetAccountName);
                    break;
            }
        }

        private void ConfirmNewCharName(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                ConfirmNewCharName("Y");
                return;
            }

            switch (char.ToUpper(argument[0]))
            {
                default:
                    DisplayRaces();
                    Write("What is your race? ");
                    SetState(GetCharRace);
                    break;

                case 'N':
                    Write("Ok, what is your characters name then? ");
                    Playing = null;
                    break;
            }
        }

        private void ConfirmNewPassword(string argument)
        {
            if (!Account.Password.ToArray().DeepCompare(Encoding.Default.GetBytes(argument)))
            {
                if (++_passwordCount > 3)
                {
                    WriteLine("Too many password attempts.  Disconnecting.");
                    Log.Warn("Too many password attempts for " + Account.Login);
                    Client.Close();
                }
                else
                {
                    Write("Passwords do not match. Try again ({0}): ", _passwordCount);
                }
                return;
            }
            WriteLine();
            Write("Please enter a valid email address: ");
            SetState(GetEmail);
            RequestEchoOn();
        }


        private void DisplayClasses()
        {
            WriteLine();
            foreach (var c in Server.World.Context.Classes)
            {
                WriteLine("{0,-10}: {1}", c.Name, c.Description);
            }
        }

        private void DisplayMotd()
        {
            WriteLine("* Don't feed the immortals.");
            WriteLine();
            WriteLine("[Hit return to continue]");

            if (Playing.Level == 0)
            {
                
                    if (Server.World.Characters.OfType<Player>().Count() == 0)
                    {
                        Playing.Level = ArtheaHelper.MaxLevel;
                        Log.Warn("Setting first player " + Playing.Name + " immortal");
                        Playing.WriteLine("You are now level " + Playing.Level + "!");
                    }
               
            }
            if (Playing.Room == null)
            {
                Playing.Room = Playing.World.GetDefaultRoom();
            }
            Playing.Room.Characters.Add(Playing);
            Server.World.Connections.Add(this);

            Log.Info(Playing.Name + " has logged in");
        }

        private void DisplayRaces()
        {
            WriteLine();

            foreach (var r in Server.World.Context.Races.OfType<PcRace>())
            {
                WriteLine("{0,-10}: {1}", r.Name, r.Description);
            }

        }

        private void GetAccountName(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                Write("Please enter a valid account name: ");
                return;
            }

            foreach (var a in Server.World.Context.Accounts)
            {
                if (!a.Login.Equals(argument, StringComparison.CurrentCultureIgnoreCase)) continue;

                Account = a;
                Account.Connection = this;
                Write("Password: ");
                RequestEchoOff();
                SetState(GetAccountPassword);
                return;
            }


            Account = new Account { Connection = this, Login = argument };
            World.Context.Accounts.AddObject(Account);
            Write("Would you like to create an account for " + argument + "? (Y/n)");
            SetState(ConfirmNewAccount);
        }

        private void GetAccountPassword(string argument)
        {
            var passwd = Encoding.Default.GetBytes(argument);

            if (!Account.Password.ToArray().DeepCompare(passwd))
            {
                if (++_passwordCount > 3)
                {
                    WriteLine("Too many password attempts.  Disconnecting.");
                    Log.Warn("Too many password attempts for " + Account.Login);
                    Client.Close();
                }
                else
                {
                    Write("Passwords do not match. Try again ({0}): ", _passwordCount);
                }
                return;
            }
            RequestEchoOn();
            AccountPlayerSelection();
            SetState(GetPlayer);
        }

        private void GetCharClass(string argument)
        {

            var cl = Server.World.Context.Classes.FirstOrDefault(c => c.Name.StartsWith(argument));

            if (cl == null)
            {
                Write("No such class! Try again: ");
                return;
            }
            Playing.Class = cl;

            Account.Players.Add(Playing);
            DisplayMotd();
            SetState(PlayingState);
        }

        private void GetCharRace(string argument)
        {
            var r = Server.World.Context.Races.OfType<PcRace>().FirstOrDefault(r2 => r2.Name.StartsWith(argument));

                if (r == null)
                {
                    Write("No such race! Try again: ");
                    return;
                }
                Playing.Race = r;
            
            DisplayClasses();
            Write("What kind of character shall you be? ");
            SetState(GetCharClass);
        }

        private void GetEmail(string argument)
        {
            if (!ArtheaHelper.IsEmail(argument))
            {
                Write("Please enter a valid email address: ");
                return;
            }

            Account.Email = argument;
            AccountPlayerSelection();
            SetState(GetPlayer);
        }

        private void GetNewCharName(string argument)
        {
            if (string.IsNullOrEmpty(argument) || argument.Length > 12)
            {
                Write("Please enter a valid character name (12 letters or less): ");
                return;
            }

            argument = argument.Capitalize();
            
                if (Server.World.Characters.OfType<Player>().FirstOrDefault(p => p.Name.StartsWith(argument)) != null)
                {
                    WriteLine("There is already a player with that name.");
                    Write("Please enter another name: ");
                    return;
                }
            

            Playing = new Player
                          {
                              Account = Account,
                              Name = argument,
                          };
            World.Context.Characters.AddObject(Playing);
            Write("Create a character named " + Playing.Name + "? (Y/n) ");
            SetState(ConfirmNewCharName);
        }

        private void GetNewPassword(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                Write("Please enter a valid password: ");
                return;
            }

            Account.Password = Encoding.Default.GetBytes(argument);
            WriteLine();
            Write("Enter password again: ");
            SetState(ConfirmNewPassword);
        }

        private void GetPlayer(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                Write("Please select a valid option: ");
                return;
            }

            if (char.ToUpper(argument[0]) == 'C')
            {
                Write("What is your character's name? ");
                SetState(GetNewCharName);
                return;
            }
            if (!argument.IsNumber())
            {
                GetPlayer(null);
                return;
            }

            var option = int.Parse(argument) - 1;

            if (option < 0 || option > Account.Players.Count)
            {
                GetPlayer(null);
                return;
            }

            Playing = Account.Players.ElementAt(option);
            DisplayMotd();
            SetState(PlayingState);
        }

        private void PlayingState(string argument)
        {
            if (HasPage())
            {
                ShowPage(argument);
                return;
            }

            if (Editing != null)
            {
                Editing.Execute(new Argument(argument));
                return;
            }

            try
            {
                Command.Interpret(Playing, new Argument(argument));
                WriteLine();
                Write("> ");
            }
            catch (QuitCommandException)
            {
                Playing.Dispose();

                Close();

                Dispose();
            }
            catch (Exception ex)
            {
                Log.Error(ex.Message);
            }
        }

        /// <summary>
        /// Requests the client does not echo.
        /// </summary>
        public void RequestEchoOff()
        {
            if (Flags.And(Connection.SuppressGoAhead))
                Client.SendGoAhead();
            Flags |= Connection.SuppressEcho;
            Client.SendWill(TelnetOption.Echo);
        }

        /// <summary>
        /// Requests the client echo's.
        /// </summary>
        public void RequestEchoOn()
        {
            if (Flags.And(Connection.SuppressGoAhead))
                Client.SendGoAhead();
            Flags &= ~Connection.SuppressEcho;
            Client.SendWont(TelnetOption.Echo);
        }


        private void SetState(StateHandler newState)
        {
            _state = newState;
        }

        #endregion

        #region Implementation of IDisposable

        /// <summary>
        ///                     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Server.World.Connections.Remove(this);
        }

        #endregion


        [Flag] public static readonly Flag SuppressEcho = Flag.MakeBit(0);
        [Flag] public static readonly Flag SuppressGoAhead = Flag.MakeBit(1);

    }
}